﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;

namespace BookSleeve
{
    internal interface IOutputStream
    {
        void Write(byte[] buffer, int offet, int count);
        void WriteByte(byte value);
    }
    internal interface IInputStream
    {
        int Read(byte[] buffer, int offset, int count);
        bool DataAvailable { get; }
    }
    internal abstract class AsyncSocket : IDisposable, IOutputStream, IInputStream
    {
        [Conditional("LOG_TO_CONSOLE")]
        protected virtual void LogToConsole(string value) { }
        [Conditional("LOG_TO_CONSOLE")]
        protected void LogToConsole(string format, params object[] args)
        {
            LogToConsole(string.Format(format, args));
        }
        public void Dispose()
        {
            Shutdown();
        }
        private Socket socket;
        private SocketAsyncEventArgs writeArgs;
        public AsyncSocket()
        {
            writeArgs = new SocketAsyncEventArgs();
            writeArgs.Completed += OnWrite;
        }
        bool IInputStream.DataAvailable
        {
            get
            {
                var tmp = socket;
                return tmp != null && tmp.Available > 0;
            }
        }
        int IInputStream.Read(byte[] buffer, int offset, int count)
        {
            var tmp = socket;
            if (tmp == null) return 0;
            return tmp.Receive(buffer, offset, count, SocketFlags.None);
        }
        private int connectState;
        const int STATE_NEW = 0, STATE_OPENING = 1, STATE_OPEN = 2, STATE_CLOSED = 3;
        public void Open(EndPoint endpoint)
        {
            if (Interlocked.CompareExchange(ref connectState, STATE_OPENING, STATE_NEW) != STATE_NEW)
            {
                throw new InvalidOperationException("The socket has already been opened");
            }
            var connectSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            
            var connect = new SocketAsyncEventArgs {RemoteEndPoint = endpoint};
            connect.UserToken = connectSocket;
            connect.Completed += OnConnect;
            if (!connectSocket.ConnectAsync(connect)) OnConnect(this, connect);
        }
        public event EventHandler<ErrorEventArgs> Error;
        private void OnError(Exception exception, string cause, bool isFatal)
        {
            var handler = Error;
            if (handler != null)  handler(this, new ErrorEventArgs(exception, cause, isFatal));
        }

        private void OnConnect(object sender, SocketAsyncEventArgs e)
        {
            try
            {
                if (e.SocketError == SocketError.Success)
                {
                    socket = (Socket)e.UserToken;
                    socket.NoDelay = true;
                    int? tmp = ReadTimeout;
                    if (tmp.HasValue) socket.ReceiveTimeout = tmp.Value;
                    tmp = WriteTimeout;
                    if (tmp.HasValue) socket.SendTimeout = tmp.Value;

                    Interlocked.CompareExchange(ref connectState, STATE_OPEN, STATE_OPENING);
                    StartReading();
                    PromptToWrite(); // only does anything if we have data buffered
                }
                else
                {
                    OnSocketError(e);
                }
            }
            catch (Exception ex)
            {
                OnError(ex, "OnConnect", true);
            }
        }
        void Shutdown()
        {
            Interlocked.Exchange(ref connectState, STATE_CLOSED);
            Socket tmp = socket;
            socket = null;
            try
            {
                using (tmp) { }
            }
            catch { }
        }
        void OnSocketError(SocketAsyncEventArgs args)
        {
            Shutdown();
            OnError(new Exception(args.SocketError.ToString()), args.LastOperation.ToString(), true);
        }

        int isSending;



        public void Flush()
        {
            if (Thread.VolatileRead(ref connectState) == STATE_CLOSED)
            {
                throw new InvalidOperationException("The socket has been closed");
            }
            LogToConsole("> Flush");
            lock (buildingSyncLock)
            {
                if (building.HasData)
                {
                    PushBuffer(); // get it sent ASAP even though not full
                }
            }
            LogToConsole("< Flush");
        }
        void IOutputStream.WriteByte(byte value)
        {
            if (Thread.VolatileRead(ref connectState) == STATE_CLOSED)
            {
                throw new InvalidOperationException("The socket has been closed");
            }
            lock (buildingSyncLock)
            {
                if (building.Append(value))
                { // full
                    PushBuffer(); // also sets "building" to a new buffer
                }
            }
        }
        void IOutputStream.Write(byte[] buffer, int offset, int count)
        {
            if (Thread.VolatileRead(ref connectState) == STATE_CLOSED)
            {
                throw new InvalidOperationException("The socket has been closed");
            }
            lock (buildingSyncLock)
            {
                while (count > 0)
                {
                    if(building.Append(buffer, ref offset, ref count))
                    { // full
                        PushBuffer(); // also sets "building" to a new buffer
                    }
                }
            }
        }
        
        // these are ready to be sent ASAP
        readonly Queue<OutboundBuffer> readyQueue = new Queue<OutboundBuffer>();
        readonly object buildingSyncLock = new object();
        OutboundBuffer building = OutboundBuffer.Create();


        struct OutboundBuffer
        {
            public bool HasData { get { return count > 0; } }
            public bool Append(byte value)
            {
                buffer[count++] = value;
                return buffer.Length == count;
            }
            public bool Append(byte[] sourceBuffer, ref int sourceOffset, ref int sourceCount)
            {
                int space = buffer.Length - count;
                int toWrite = space > sourceCount ? sourceCount : space;
                System.Buffer.BlockCopy(sourceBuffer, sourceOffset, buffer, count, toWrite);

                sourceOffset += toWrite;
                sourceCount -= toWrite;
                count += toWrite;

                return buffer.Length == count;
            }
            public void SetBuffer(SocketAsyncEventArgs args)
            {
                args.SetBuffer(buffer, 0, count);
            }
            private int count;
            private readonly byte[] buffer;
            public static OutboundBuffer Create()
            {
                return new OutboundBuffer(BufferPool.GetBuffer());
            }
            private OutboundBuffer(byte[] buffer)
            {
                this.buffer = buffer;
                this.count = 0;
            }
        }

        private void PushBuffer()
        {
            lock (readyQueue)
            {
                readyQueue.Enqueue(building);
            }
            building = OutboundBuffer.Create();

            PromptToWrite();
        }
        public void PromptToWrite()
        {
            if (Thread.VolatileRead(ref connectState) == STATE_OPEN && Interlocked.CompareExchange(ref isSending, 1, 0) == 0)
            {
                LogToConsole("Took write-token to prompt");
                if (PrepareNextPending(writeArgs))
                { // we got the conch!
                    LogToConsole("Sending {0} bytes from PromptToWrite", writeArgs.Count);
                    if (!socket.SendAsync(writeArgs)) OnWrite(this, writeArgs);
                }
            }
        }

        bool PrepareNextPending(SocketAsyncEventArgs args)
        {
            // try to use data from the ready-queue first
            lock (readyQueue)
            {
                if(readyQueue.Count != 0)
                {
                    readyQueue.Dequeue().SetBuffer(args);
                    return true;
                }
            }

            // pull some messages down to try to fill the buffer to the point where the
            // ready-queue has some data; note we need to surrender our reading flag
            // in case something (constraints) needs to send more urgently
            Interlocked.Exchange(ref isSending, 0);
            LogToConsole("Relinquished write-token to request data");
            while(RequestData())
            {
                lock(readyQueue)
                {
                    if(readyQueue.Count != 0 && Interlocked.CompareExchange(ref isSending, 1, 0) == 0)
                    {
                        LogToConsole("Took write-token after requesting data");
                        readyQueue.Dequeue().SetBuffer(args);
                        return true;
                    }
                }
            }

            // if we're still needed, try flushing
            if(Interlocked.CompareExchange(ref isSending, 1, 0) == 0)
            {
                LogToConsole("Took write-token prior to flush");
                Flush();
                lock(readyQueue)
                {
                    if(readyQueue.Count != 0)
                    {
                        readyQueue.Dequeue().SetBuffer(args);
                        return true;
                    }
                    Interlocked.Exchange(ref isSending, 0);
                    LogToConsole("Relinquished write-token; nothing after flush");
                }
            }

            // no data, or someone else is pumping
            return false;
        }

        protected virtual bool RequestData()
        {
            return false;
        }

        private void OnWrite(object sender, SocketAsyncEventArgs e)
        {
            try
            {
                bool writeMore;
                do
                {
                    writeMore = false;
                    if (e.SocketError == SocketError.Success)
                    {
                        if (e.BytesTransferred == e.Count)
                        {
                            BufferPool.Recycle(e.Buffer);
                            if (PrepareNextPending(e))
                            {
                                LogToConsole("send: {0} bytes", e.Count);
                                writeMore = !socket.SendAsync(e);
                            }
                        }
                        else
                        { // finish that packet!

                            e.SetBuffer(e.Buffer, e.Offset + e.BytesTransferred, e.Count - e.BytesTransferred);
                            Console.WriteLine("cont: {0} bytes", e.Count);
                            writeMore = !socket.SendAsync(e);
                        }
                    }
                    else
                    {
                        Interlocked.Exchange(ref isSending, 0);
                        LogToConsole("Relinquished write-token due to e socket error in OnWrite: {0}", e.SocketError);
                        OnSocketError(e);
                    }
                } while (writeMore);
            }
            catch (Exception ex)
            {
                Interlocked.Exchange(ref isSending, 0);
                LogToConsole("Relinquished write-token due to exception in OnWrite: {0}", ex.Message);
                OnError(ex, "OnWrite", true);
            }
        }
        void StartReading()
        {
            var read = new SocketAsyncEventArgs();
            read.Completed += OnRead;
            var buffer = BufferPool.GetBuffer();
            read.SetBuffer(buffer, 0, buffer.Length);
            if (!socket.ReceiveAsync(read)) OnRead(this, read);
        }

        private void OnRead(object sender, SocketAsyncEventArgs e)
        {
            try
            {
                bool readMore;
                do
                {
                    readMore = false;
                    if (e.SocketError == SocketError.Success)
                    {
                        bool keepReading = ProcessIncoming(e.Buffer, e.Offset, e.BytesTransferred) && e.BytesTransferred != 0;
                        if (keepReading)
                        {
                            readMore = !socket.ReceiveAsync(e);
                        }
                        else
                        {
                            BufferPool.Recycle(e.Buffer);
                            Shutdown();
                            readMore = false;
                        }
                    }
                    else
                    {
                        ProcessIncoming(null, 0, 0); // signal EOF via error
                        OnSocketError(e);
                    }
                } while (readMore);
            }
            catch (Exception ex)
            {
                OnError(ex, "OnRead", true);
            }
        }

        protected abstract bool ProcessIncoming(byte[] buffer, int offset, int count);

        private static class BufferPool
        {
            const int BUFFER_COUNT = 50, BUFFER_SIZE = 1024;
            static byte[][] bufferPool = new byte[BUFFER_COUNT][];
            public static byte[] GetBuffer()
            {
                byte[] found;
                for (int i = 0; i < BUFFER_COUNT; i++)
                {
                    if ((found = Interlocked.Exchange(ref bufferPool[i], null)) != null) return found;
                }
                return new byte[BUFFER_SIZE];
            }
            public static void Recycle(byte[] buffer)
            {
                if (buffer != null)
                {
                    for (int i = 0; i < BUFFER_COUNT; i++)
                    {
                        if (Interlocked.CompareExchange(ref bufferPool[i], buffer, null) == null) return;
                    }
                }
                // drop it on the floor
            }
        }
        public int? WriteTimeout { get; set; }
        public int? ReadTimeout { get; set; }
    }
    
}
